// --- BEGIN COPYRIGHT BLOCK ---
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// (C) 2007 Red Hat, Inc.
// All rights reserved.
// --- END COPYRIGHT BLOCK ---
package com.netscape.cms.profile.constraint;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Locale;

import org.dogtagpki.server.ca.CAEngine;
import org.mozilla.jss.netscape.security.x509.CRLExtensions;
import org.mozilla.jss.netscape.security.x509.CRLReasonExtension;
import org.mozilla.jss.netscape.security.x509.CertificateExtensions;
import org.mozilla.jss.netscape.security.x509.CertificateSubjectName;
import org.mozilla.jss.netscape.security.x509.Extension;
import org.mozilla.jss.netscape.security.x509.KeyUsageExtension;
import org.mozilla.jss.netscape.security.x509.RevocationReason;
import org.mozilla.jss.netscape.security.x509.X509CertImpl;
import org.mozilla.jss.netscape.security.x509.X509CertInfo;

import com.netscape.certsrv.base.EBaseException;
import com.netscape.certsrv.dbs.certdb.CertId;
import com.netscape.certsrv.profile.EProfileException;
import com.netscape.certsrv.profile.ERejectException;
import com.netscape.certsrv.property.Descriptor;
import com.netscape.certsrv.property.IDescriptor;
import com.netscape.cms.profile.common.PolicyConstraintConfig;
import com.netscape.cms.profile.def.NoDefault;
import com.netscape.cms.profile.def.PolicyDefault;
import com.netscape.cms.profile.def.SubjectNameDefault;
import com.netscape.cms.profile.def.UserSubjectNameDefault;
import com.netscape.cmscore.apps.CMS;
import com.netscape.cmscore.dbs.CertRecord;
import com.netscape.cmscore.dbs.CertificateRepository;
import com.netscape.cmscore.dbs.RevocationInfo;
import com.netscape.cmscore.request.Request;

/**
 * This class implements the unique subject name constraint.
 * It checks if the subject name in the certificate is
 * unique in the internal database, ie, no two certificates
 * have the same subject name.
 */
public class UniqueSubjectNameConstraint extends EnrollConstraint {

    public static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(UniqueSubjectNameConstraint.class);

    public static final String CONFIG_KEY_USAGE_EXTENSION_CHECKING =
            "enableKeyUsageExtensionChecking";
    private boolean mKeyUsageExtensionChecking = true;

    public UniqueSubjectNameConstraint() {
        addConfigName(CONFIG_KEY_USAGE_EXTENSION_CHECKING);
    }

    public void init(PolicyConstraintConfig config) throws EProfileException {
        super.init(config);

        mKeyUsageExtensionChecking = getConfigBoolean(CONFIG_KEY_USAGE_EXTENSION_CHECKING);
        logger.info("UniqueSubjectNameConstraint: Key usage extension checking: " + mKeyUsageExtensionChecking);
    }

    @Override
    public IDescriptor getConfigDescriptor(Locale locale, String name) {
        if (name.equals(CONFIG_KEY_USAGE_EXTENSION_CHECKING)) {
            return new Descriptor(IDescriptor.BOOLEAN, null, "true",
                    CMS.getUserMessage(locale, "CMS_PROFILE_CONFIG_KEY_USAGE_EXTENSION_CHECKING"));
        }
        return null;
    }

    /**
     * Checks if the key extension in the issued certificate
     * is the same as the one in the certificate template.
     */
    private boolean sameKeyUsageExtension(CertRecord rec,
            X509CertInfo certInfo) {
        X509CertImpl impl = rec.getCertificate();
        boolean bits[] = impl.getKeyUsage();

        CertificateExtensions extensions = null;

        try {
            extensions = (CertificateExtensions)
                    certInfo.get(X509CertInfo.EXTENSIONS);
        } catch (IOException e) {
        } catch (java.security.cert.CertificateException e) {
        }
        KeyUsageExtension ext = null;

        if (extensions == null) {
            if (bits != null)
                return false;
        } else {
            try {
                ext = (KeyUsageExtension) extensions.get(
                        KeyUsageExtension.NAME);
            } catch (IOException e) {
                // extension isn't there.
            }

            if (ext == null) {
                if (bits != null)
                    return false;
            } else {
                boolean[] InfoBits = ext.getBits();

                if (InfoBits == null) {
                    if (bits != null)
                        return false;
                } else {
                    if (bits == null)
                        return false;
                    if (InfoBits.length != bits.length) {
                        return false;
                    }
                    for (int i = 0; i < InfoBits.length; i++) {
                        if (InfoBits[i] != bits[i])
                            return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * Validates the request. The request is not modified
     * during the validation.
     *
     * Rules are as follows:
     * If the subject name is not unique, then the request will be rejected unless:
     * 1. the certificate is expired or expired_revoked
     * 2. the certificate is revoked and the revocation reason is not "on hold"
     * 3. the keyUsageExtension bits are different and enableKeyUsageExtensionChecking=true (default)
     */
    @Override
    public void validate(Request request, X509CertInfo info)
            throws ERejectException {

        CertificateSubjectName sn;
        try {
            sn = (CertificateSubjectName) info.get(X509CertInfo.SUBJECT);
        } catch (Exception e) {
            throw new ERejectException(
                    CMS.getUserMessage(getLocale(request),
                            "CMS_PROFILE_SUBJECT_NAME_NOT_FOUND"), e);
        }

        logger.info("UniqueSubjectNameConstraint: Validating " + sn);

        if (sn == null)
            throw new ERejectException(
                    CMS.getUserMessage(getLocale(request),
                            "CMS_PROFILE_SUBJECT_NAME_NOT_FOUND"));

        CAEngine engine = CAEngine.getInstance();
        CertificateRepository certdb = engine.getCertificateRepository();

        String certsubjectname = sn.toString();
        String filter = "x509Cert.subject=" + certsubjectname;
        logger.info("UniqueSubjectNameConstraint: Searching for " + filter);

        Enumeration<CertRecord> sameSubjRecords = null;
        try {
            sameSubjRecords = certdb.findCertRecords(filter);
        } catch (EBaseException e) {
            logger.warn("UniqueSubjectNameConstraint: " + e.getMessage(), e);
            throw new ERejectException(e);
        }

        logger.info("UniqueSubjectNameConstraint: Results: ");

        while (sameSubjRecords != null && sameSubjRecords.hasMoreElements()) {
            CertRecord rec = sameSubjRecords.nextElement();
            logger.info("UniqueSubjectNameConstraint: - serial number: " + new CertId(rec.getSerialNumber()).toHexString());

            String status = rec.getStatus();
            logger.info("UniqueSubjectNameConstraint:   status: " + status);

            if (status.equals(CertRecord.STATUS_EXPIRED) || status.equals(CertRecord.STATUS_REVOKED_EXPIRED)) {
                logger.info("UniqueSubjectNameConstraint:   expired -> skip");
                continue;
            }

            RevocationInfo revocationInfo = rec.getRevocationInfo();
            RevocationReason reason = null;

            if (revocationInfo != null) {
                CRLExtensions crlExts = revocationInfo.getCRLEntryExtensions();

                if (crlExts != null) {
                    Enumeration<Extension> enumx = crlExts.getElements();

                    while (enumx.hasMoreElements()) {
                        Extension ext = enumx.nextElement();

                        if (ext instanceof CRLReasonExtension) {
                            reason = ((CRLReasonExtension) ext).getReason();
                        }
                    }
                }

                logger.info("UniqueSubjectNameConstraint:   reason: " + reason);
            }

            if (status.equals(CertRecord.STATUS_REVOKED) && reason != null &&
                    (!reason.equals(RevocationReason.CERTIFICATE_HOLD))) {
                logger.info("UniqueSubjectNameConstraint:   revoked -> skip");
                continue;
            }

            if (mKeyUsageExtensionChecking && !sameKeyUsageExtension(rec, info)) {
                logger.info("UniqueSubjectNameConstraint:   different key usage -> skip");
                continue;
            }

            logger.error("UniqueSubjectNameConstraint: Subject name is not unique");

            throw new ERejectException(
                    CMS.getUserMessage(getLocale(request),
                            "CMS_PROFILE_SUBJECT_NAME_NOT_UNIQUE",
                            certsubjectname));
        }

        logger.info("UniqueSubjectNameConstraint: Subject name is unique");
    }

    @Override
    public String getText(Locale locale) {
        String params[] = {
                getConfig(CONFIG_KEY_USAGE_EXTENSION_CHECKING)
        };
        return CMS.getUserMessage(locale,
                "CMS_PROFILE_CONSTRAINT_UNIQUE_SUBJECT_NAME_TEXT",
                params);
    }

    @Override
    public boolean isApplicable(PolicyDefault def) {
        if (def instanceof NoDefault)
            return true;
        if (def instanceof SubjectNameDefault)
            return true;
        if (def instanceof UserSubjectNameDefault)
            return true;
        return false;
    }
}
